﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Prng;

namespace LockCrypt.Core {
    public class PasswordGenerator {
        public static readonly char[] BracketChars = new[] { '(', ')', '[', ']', '{', '}', '<', '>' },
                                      DigitChars = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
                                      UpperCaseChars = new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' },
                                      LowerCaseChars = new[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' },
                                      SpecialChars = new[] { '!', '"', '£', '$', '€', '%', '^', '&', '*', '-', '_', '=', '+', '@', '#', '~', '?', '`', '/', '\\', '\'' },
                                      ExtendedChars = Enumerable.Range(161, (255 - 161)).Select(i => (char)i).ToArray();

        /// <summary>
        /// Generates passwords.
        /// </summary>
        /// <param name="specification">The password specification.</param>
        /// <param name="count">The number to generate.</param>
        /// <returns>The specified number of passwords matching the format.</returns>
        public static IEnumerable<string> GeneratePasswords(PasswordCreationArgs specification, int count) {
            IEnumerable<char> possibleChars = GetCharacterPool(specification.Brackets, specification.Digits, specification.UpperCase, specification.LowerCase, specification.ExtendedAscii, specification.SpecialCharacters, specification.ExtraCharacters);
            SecureRandom seed = new SecureRandom(new CryptoApiRandomGenerator());
            char[] possChars = possibleChars.ToArray();
            int numPossChars = possChars.Length;
            // Fisher-Yates shuffle
            int highestIndex = numPossChars-1;
            for(int i = numPossChars - 1; i > 0; i--) {
                int pos = seed.Next(0, highestIndex);
                char temp = possChars[i];
                possChars[i] = possChars[pos];
                possChars[pos] = temp;
            }
            List<string> generatedPasswords = new List<string>();
            if(specification.PasswordMask != null) {
                if(possChars.Length == 0) {
                    throw new LockCryptException(I18nUtils.GetString("Errors", "NoEligibleCharactersFormat"));
                }
                string validationError = ValidateMask(specification);
                if(validationError != null) {
                    throw new LockCryptException(validationError);
                }
                generatedPasswords.Add(GeneratePassword(possChars, specification.PasswordMask, specification.Length, seed));
            } else {
                if(possChars.Length == 0) {
                    throw new Exception(I18nUtils.GetString("Errors", "NoEligibleCharacters"));
                }
                for(int i=0;i<count; i++) {
                    generatedPasswords.Add(GeneratePassword(possChars, specification.Length, seed));
                }
            }
            return generatedPasswords;
        }

        /// <summary>
        /// Gets available characters for a password specification.
        /// </summary>
        /// <param name="brackets"><c>true</c> if brackets should be included, otherwise <c>false</c>.</param>
        /// <param name="digits"><c>true</c> if digits should be included, otherwise <c>false</c>.</param>
        /// <param name="uppercase"><c>true</c> if uppercase should be included, otherwise <c>false</c>.</param>
        /// <param name="lowercase"><c>true</c> if lowercase should be included, otherwise <c>false</c>.</param>
        /// <param name="extendedAscii"><c>true</c> if extended ASCII should be included, otherwise <c>false</c>.</param>
        /// <param name="specialCharacters"><c>true</c> if special characters should be included, otherwise <c>false</c>.</param>
        /// <param name="extraCharacters">Extra characters to include, or <c>null</c>.</param>
        /// <returns>A list of characters from the specified characters sets.</returns>
        private static List<char> GetCharacterPool(bool brackets, bool digits, bool uppercase, bool lowercase, bool extendedAscii, bool specialCharacters, IEnumerable<char> extraCharacters) {
            List<char> possibleChars = new List<char>();
            if(brackets) {
                possibleChars.AddRange(BracketChars);
            }
            if(digits) {
                possibleChars.AddRange(DigitChars);
            }
            if(uppercase) {
                possibleChars.AddRange(UpperCaseChars);
            }
            if(lowercase) {
                possibleChars.AddRange(LowerCaseChars);
            }
            if(specialCharacters) {
                possibleChars.AddRange(SpecialChars);
            }
            if(extendedAscii) {
                possibleChars.AddRange(ExtendedChars);
            }
            if(extraCharacters != null) {
                possibleChars.AddRange(extraCharacters);
            }
            return possibleChars.Distinct().ToList();
        }

        /// <summary>
        /// Generates a password.
        /// </summary>
        /// <param name="characterPool">The character pool.</param>
        /// <param name="length">The length.</param>
        /// <param name="seed">The random seed.</param>
        /// <returns>
        /// A password of the specified length from characters in the pool.
        /// </returns>
        public static string GeneratePassword(char[] characterPool, int length, SecureRandom seed) {
            int numPossible = characterPool.Count();
            if(numPossible == 0) {
                throw new ArgumentOutOfRangeException("characterPool", "No characters selected");
            } else if(length <=0) {
                throw new ArgumentOutOfRangeException("length", "Length must be greater than zero");
            }
            StringBuilder pass = new StringBuilder(length);
            int highestIndex = numPossible-1;
            for(int i = 0; i < length; i++) {
                pass.Append(characterPool[seed.Next(0, highestIndex)]);
            }
            return pass.ToString();
        }

        /// <summary>
        /// Validates and corrects a password mask against the characters selected.
        /// </summary>
        /// <param name="specification">The password specification.</param>
        /// <returns><c>null</c> if the password is valid, a string containing the reason a password is invalid if it's not.</returns>
        private static string ValidateMask(PasswordCreationArgs specification) {
            string validationError = null;
            if(string.IsNullOrEmpty(specification.PasswordMask)) {
                validationError = "Mask empty";
            } else {
                StringBuilder buffer = new StringBuilder(specification.PasswordMask.Replace(" ", string.Empty));
                if(!specification.Digits) {
                    buffer.Replace("d", "*");
                }
                if(!specification.LowerCase) {
                    buffer.Replace("c", "*");
                }
                if(!specification.UpperCase) {
                    buffer.Replace("C", "*");
                }
                if(!specification.Brackets) {
                    buffer.Replace("b", "*");
                }
                if(!specification.SpecialCharacters) {
                    buffer.Replace("s", "*");
                }
                if(!specification.ExtendedAscii) {
                    buffer.Replace("e", "*");
                }
                if(buffer.Length == 0) {
                    validationError = I18nUtils.GetString("Errors", "NoEligibleCharactersFormat");
                }
                specification.PasswordMask = buffer.ToString();
                /*bool haveValidChars = specification.Digits && specification.PasswordMask.Contains('d') ||
                                      specification.LowerCase && specification.PasswordMask.Contains('c') ||
                                      specification.UpperCase && specification.PasswordMask.Contains('C') ||
                                      specification.Brackets && specification.PasswordMask.Contains('b') ||
                                      specification.SpecialCharacters && specification.PasswordMask.Contains('s') ||
                                      specification.ExtendedAscii && specification.PasswordMask.Contains('e');
                if(!haveValidChars) {
                    validationError = I18nUtils.GetString("Errors", "NoEligibleCharactersFormat");
                }*/
            }
            return validationError;
        }

        /// <summary>
        /// Generates a password using a password mask.
        /// </summary>
        /// <param name="characterPool">The character pool.</param>
        /// <param name="mask">The password mask.</param>
        /// <param name="length">The length.</param>
        /// <param name="seed">The random seed.</param>
        /// <returns>
        /// A password of the specified length from characters in the pool.
        /// </returns>
        public static string GeneratePassword(char[] characterPool, string mask, int length, SecureRandom seed) {
            int numPossible = characterPool.Count();
            if(numPossible == 0) {
                throw new ArgumentOutOfRangeException("characterPool", "No characters selected");
            } else if(length <= 0) {
                throw new ArgumentOutOfRangeException("length", "Length must be greater than zero");
            }
            int highestIndex = numPossible - 1;
            StringBuilder password = new StringBuilder(length);
            int i = 0;
            while(i < length) {
                char proposed = characterPool[seed.Next(0, highestIndex)];
                if(mask.Length > i) {// can only use the mask to generate while it's long enough
                    switch(mask[i]) {
                        case 'd':
                            if(DigitChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        case 'c':
                            if(LowerCaseChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        case 'C':
                            if(UpperCaseChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        case 'b':
                            if(BracketChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        case 's':
                            if(SpecialChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        case 'e':
                            if(ExtendedChars.Contains(proposed)) {
                                password.Append(proposed);
                                i++;
                            }
                            break;
                        default:
                            password.Append(proposed);
                            i++;
                            break;
                    }
                } else {
                    password.Append(proposed);
                    i++;
                }
            }
            return password.ToString();
        }
    }
}
